/**
 * 热更模块
 * PackageManager
 * 仅做大厅和子游戏热更相关管理，实现多模块后台同步下载更新。HotUpdate对象的管理
 * HotUpdate
 * 单模块更新操作,如果更新失败的时候，会自动重试MAX_RETRY次，超过MAX_RETRY次将不再进行主动重试，需要外部主动操作。
 */
import { App, UpdateState } from "../App";
import { Matrix, Singleton } from "./Class";
import { EventConst } from "./EventConst";
import Map from "./Map";

const VERSION_FILE = 'version.manifest';
const PROJECT_FILE = 'project.manifest'
const DEFAULT_VERSION = '0.0.0'; // 初始默认版本
const MAX_RETRY = 10;

class HotUpdate extends Matrix{
    private _mod:string = '';
    private _amObj:jsb.AssetsManager = null;
    private _retryCount:number = 0;

    constructor(){
        super();
    }

    protected onInit(mod:string){
        if (!jsb) return ;
        this._mod = mod;
        const storagePath = this._mod !== App.MOD_NAME.MASTER ? `${App.writePath}${mod}/`:`${App.writePath}`;
        const manifestFile = `assets/${this._mod === App.MOD_NAME.MASTER ? PROJECT_FILE:`${App.ModuleName}/${PROJECT_FILE}`}`;
        console.log(`<------${manifestFile}----->`);
        console.log(`<------${storagePath}----->`);
        if (!jsb.fileUtils.isFileExist(manifestFile)){
            console.log(`<------${manifestFile} is not Exist----->`);
            if (mod === App.MOD_NAME.MASTER){
                App.MessageBox(App.getText('update.invalid_apk'),(flag:number)=>{
                    if (mod === App.MOD_NAME.MASTER){
                        flag === App.MB_FLAG.OK ? cc.sys.openURL(App.getCfg('downloadAPP')):cc.game.end();
                    }
                });
                return ;
            }
        }
        this._amObj = new jsb.AssetsManager(manifestFile, storagePath,(localVer:string, remoteVer:string)=>{
            let lv = localVer.split('.'); let rv = remoteVer.split('.');
            if (rv.length > lv.length) return -1;
            for (let i = 0; i < lv.length; ++i) {
                let a = parseInt(lv[i]); let b = parseInt(rv[i]) || 0;
                if (a === b) continue; else return -1;
            }
            
            return 0;
        });

        console.log(`getManifestFileUrl:${this._amObj.getLocalManifest().getManifestFileUrl()}`);
        this._amObj.setVerifyCallback((path: string, asset: any)=>{
            console.log('setVerifyCallback:',path);
            // When asset is compressed, we don't need to check its md5, because zip file have been deleted.
            let compressed = asset.compressed;
            // Retrieve the correct md5 value.
            let expectedMD5 = asset.md5;
            // asset.path is relative path and path is absolute.
            let relativePath = asset.path;
            // The size of asset file, but this value could be absent.
            let size = asset.size;
            if (compressed) {
                console.log(`Verification passed : ${relativePath}---${App.Util.formatByte(size)}`);
                return true;
            }
            else {
                console.log(`Verification passed : ${relativePath}(${expectedMD5})---${App.Util.formatByte(size)}`);
                return true;
            }
        });

        // 如果是剔除子包的情况，是没有project.manifest和version.manifest两个文件的
        if (this._amObj.getState() === jsb.AssetsManager.State.UNINITED) {
            if (mod !== App.MOD_NAME.MASTER){
                const url = `${App.getCfg('updateURL')}/${mod}/`;
                const manifest = JSON.stringify({
                    packageUrl: url,
                    remoteManifestUrl: `${url}${PROJECT_FILE}`,
                    remoteVersionUrl: `${url}${VERSION_FILE}`,
                    version: DEFAULT_VERSION,
                    assets: {},
                    searchPaths: []
                });
                this._amObj.loadLocalManifest(new jsb.Manifest(manifest, storagePath), storagePath);
            }
        }
        
        let _index = 0;
        this._amObj.setVerifyCallback((path, asset) => {
            console.log(`${++_index}. Finish file:  `, asset.path);
            console.log('abs path:', path);
            return true;
        });
        
        jsb.fileUtils.removeDirectory(`${App.Native.getWritablePath()}/${this._mod}_temp`);
        // 开始更新操作
        this._retryCount = 0;
        this._amObj.setEventCallback(this._updateCall.bind(this));

        console.log(`UpdateModuleBegin:${this._mod}`);
        this._retryCount = 0;
        this._amObj.update();
    }
    protected onDestroy(){
        this._amObj.setEventCallback(null);
        let _am = this._amObj;
        _am = null;
    }

    private _updateCall(event:jsb.EventAssetsManager){
        let _str = event.getMessage();
        let _tips:string = '';
        let _failed = false;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.UPDATE_PROGRESSION:// 显示进度                
                let _cur = event.getDownloadedBytes() || 0;
                let _all = event.getTotalBytes() || 0;
                let _percent = event.getPercent() || 0;
                let _msg = event.getMessage()||'';
                App.Event.emit(EventConst.UPDATE_MODULE,this._mod,UpdateState.progress,_cur, _all,_percent,_msg);
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                App.Event.emit(EventConst.UPDATE_MODULE,this._mod,UpdateState.skip);
                break;
            case jsb.EventAssetsManager.UPDATE_FINISHED:{
                // 大厅更新结束，必须重启
                if (this._mod === App.MOD_NAME.MASTER){
                    this._amObj.setEventCallback(null);
                    // Prepend the manifest's search path
                    /*let searchPaths = jsb.fileUtils.getSearchPaths();
                    let newPaths = this._amObj.getLocalManifest().getSearchPaths();
                    console.log(JSON.stringify(newPaths));
                    Array.prototype.unshift.apply(searchPaths, newPaths);
                    // This value will be retrieved and appended to the default search path during game startup,
                    // please refer to samples/js-tests/main.js for detailed usage.
                    // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
                    
                    let _tmpArray = [];
                    let _localArray = [];
                    if (searchPaths.length > 1){
                        for (let i = 0; i < searchPaths.length; i++) {
                            const item = searchPaths[i];
                            if (item.length < 20) {
                                _localArray.indexOf(item) < 0 && _localArray.push(item);
                            } else {
                                _tmpArray.indexOf(item) < 0 && _tmpArray.push(item);
                            }
                        }
                        searchPaths = null;
                        searchPaths = _tmpArray.concat(_localArray);
                    }
                    jsb.fileUtils.setSearchPaths(searchPaths);
                    App.setLocalStorage(searchPaths,App.LOCAL_KEY.WRITE_DIR);*/
                    setTimeout(() => {
                        cc.game.restart();
                    }, 1000);
                    return ;
                }
                console.log(`------------------------>${this._mod} update finish<------------------------`);
                jsb.fileUtils.setSearchPaths(jsb.fileUtils.getSearchPaths());
                App.Event.emit(EventConst.UPDATE_MODULE,this._mod,UpdateState.success,event.getMessage());
                break;
            }
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                _tips = 'update/findLocalVerErr';
                _failed = true;
                break;
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                _tips = 'update/downloadVer';
                _failed = true;
                break;
            case jsb.EventAssetsManager.UPDATE_FAILED:
                _str = `Update failed:${event.getMessage()}`;
                if (++this._retryCount < MAX_RETRY){
                    this._amObj.downloadFailedAssets();
                    return ;
                } 
                break;
            case jsb.EventAssetsManager.ERROR_UPDATING:
                _str = `Asset update error:${event.getAssetId()}->[${event.getMessage()}]`;
                _tips = 'update/error';
                break;
            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                _str = event.getMessage();
                break;
            default:
                break;
        }

        _str && console.log(_str);
        if (_failed) {
            this._amObj.setEventCallback(null);
            App.Event.emit(EventConst.UPDATE_MODULE,this._mod,UpdateState.fail,_str,_tips);
        }
    }
}

const RES:string = 'resources';
export default class AssetsManager extends Singleton<AssetsManager>{
    private _mainBundle:cc.AssetManager.Bundle = null;
    private _resCache:any = {};
    private _packName:string = '';
    private _updateList:{mod:string,hot:HotUpdate}[] = [];
    private _remoteCfg:any = null;
    private _assetsCache:Map<string,Map<string,cc.Asset[]>> = new Map<string,Map<string,cc.Asset[]>>(); 
    private _curScene:string = '';

    constructor(){
        super();
        this._resCache[RES] = {};
    }

    onDestroy(){
        this._resCache = {};
        this._resCache[RES] = {};
        this._mainBundle = null;
        this._packName = '';
        this._updateList = [];
        this._remoteCfg = null;
        this._assetsCache = new Map<string,Map<string,cc.Asset[]>>(); 
        this._curScene = '';
    }

    onInit(){
        // 只是把模块压入管理器
        App.Event.on(EventConst.CHECK_MODULE,this._checkModule,this);

        App.Event.on(EventConst.UPDATE_MODULE,(mod:string,state:UpdateState)=>{
            if (state === UpdateState.success){
                for (let key in this._updateList){
                    let it = this._updateList[key];
                    if (it.mod === mod){
                        it.hot.destroy();
                        this._updateList.splice(parseInt(key),1);
                        break ;
                    }
                }
                if (this._updateList.length > 0){
                    let _obj = this._updateList.shift();
                    App.Util.isValid(_obj) && _obj.hot.init(_obj.mod);
                }
            }
        },this);

        App.Event.on(EventConst.DOWNLOAD_MODULE,(name:string)=>{
            (new HotUpdate()).init(name);
        },this);

        App.Event.on(EventConst.ENTRY_SCENE,(scene: string)=>{
            let subs = scene ? scene.split(':'):[];
            const flag = subs[1];
            if (App.Util.isEmpty(flag)) return ;

            if (!App.Util.isEmpty(this._curScene)){
                const old = this._assetsCache.find(this._curScene);
                old && old.iterable.forEach((it)=>{
                    while(it.Val.length > 0){
                        cc.assetManager.releaseAsset(it.Val.shift());
                    }
                });
                this._curScene = scene;
            }

        },this);
    }

    private _checkModule(name:string,state:UpdateState){
        if (!cc.sys.isNative){
            App.Event.emit(EventConst.UPDATE_MODULE,name,UpdateState.skip);
            return ;
        }
        if (state) return ;
        const _localVer = App.getCfg(`modules/${name}/version`);
        let _check = ()=>{
            const _remoteVer = this._remoteCfg['modules'][name]['version'];
            console.log(`local version:${_localVer}----remote version:${_remoteVer}`);
            let lv = _localVer.split('.'); let rv = _remoteVer.split('.');
            if (name === App.MOD_NAME.MASTER && (lv.length !== rv.length || lv[0] !== rv[0])){
                // 需要重新安装
                App.Event.emit(EventConst.CHECK_MODULE,UpdateState.reinstall);
                return ;
            }
            
            for (let i = 0; i < lv.length; ++i) {
                let a = parseInt(lv[i]); let b = parseInt(rv[i]) || 0;
                if (a !== b){
                    for (let it of this._updateList){
                        if (it.mod === name)return ;
                    }
                    if (this._updateList.length > 0){
                        this._updateList.push({mod:name,hot:(new HotUpdate())});
                        return ;
                    }
                    (new HotUpdate()).init(name);
                    return ;
                }
            }
            console.log(`module=${name} skip`)
            App.Event.emit(EventConst.UPDATE_MODULE,name,UpdateState.skip);
        };

        if (App.Util.isInvalid(this._remoteCfg)){
            const url = `${App.getCfg('updateURL')}config.json`;
            console.log(`AsssteMgr:updateURL=${url}`);
            App.http(url,null,true,(is:boolean,data:any)=>{
                if (!is){
                    App.popTips(App.getText('update.abnormal'));
                    // 热更服务器嗝屁了，直接跳过进入游戏吧
                    App.Event.emit(EventConst.UPDATE_MODULE,name,UpdateState.skip);
                    return ;
                }
                this._remoteCfg = JSON.parse(data);
                _check();
            });
            return ;
        }

        _check();
    }

    public release(name:string){
        let groups = Object.keys(this._resCache);
        for (let it of groups){
            let lst = this._resCache[it];
            let keys = Object.keys(lst);
            for (let n of keys){
                if (n === name){
                    this._mainBundle.release(name);
                    this._resCache[it][name] = null;
                    return ;
                }
            }
        }
    }

    public setMainPack(name:string,cb?:(is:boolean)=>void){
        if (this._mainBundle) return ;
        cc.assetManager.loadBundle(name,(e:Error,bundle:cc.AssetManager.Bundle)=>{
            if (e){
                cb && cb(false);
                return ;
            }
            this._packName = name;
            this._mainBundle = bundle;
            cb && cb(true);
        });
    }

    // 加载assets/resources目录下的资源接口
    public loadResources(path:string,type: typeof cc.Asset,cb?:(res:any)=>void){
        if (this._resCache[RES] && this._resCache[RES][path]){
            cb && cb(this._resCache[RES][path]);
            return ;
        }

        cc.assetManager.resources.load(path,type,(e:Error, res:cc.Asset)=>{
            if (e || !res){
                cc.error(`${path} is error.`);
                return ;
            }
            this._resCache[RES][path] = res;
            cb && cb(res);
        });
    }

    public loadRes(path:string,type: typeof cc.Asset,cb?:(res:any)=>void){
        let subs = path.split(':')
        if (subs.length !== 2) {
            cc.error(`ResMgr.loadRes(${path}) is error./n eg:'name:login|hall|room'`);
            return;
        }

        if (!this._mainBundle){
            cc.error(`ResMgr.loadRes(${path}) is error.please call function ResMgr.setMainPack()`);
            return ;
        }

        let _path = `${this._packName}/res/${subs[0]}`;
        if (this._resCache[`${subs[1]}`] && this._resCache[`${subs[1]}`][_path]){
            cb && cb(this._resCache[`${subs[1]}`][_path]);
            return ;
        }
        !this._resCache[`${subs[1]}`] && (this._resCache[`${subs[1]}`] = {});
        this._mainBundle.load(`res/${subs[0]}`,type,(e:Error,res:any)=>{
            if (e || !res){
                cc.error(`res/${subs[0]} is error.`);
                return ;
            }
            this._resCache[`${subs[1]}`][_path] = res;
            cb && cb(res);
        });
    }

    public loadRemote(URL:string,cb?:(res:cc.Asset)=>void){
        if (this._resCache['remote'] && this._resCache['remote'][URL]){
            cb && cb(this._resCache['remote'][URL]);
            return ;
        }
        if (URL.indexOf('http://') !== -1 && URL.indexOf('https://') !== -1){
            cc.error(`AssetsMgr.loadRemote(${URL}) error.`);
            return ;
        } else {
            cc.assetManager.loadRemote(URL, (e:Error, res:cc.Asset) => {
                if (e || !res){
                    cc.error(`ResMgr.loadRemote(${URL}) is error.`);
                    return ;
                }
                !this._resCache['remote'] && (this._resCache['remote'] = {});
                this._resCache['remote'][URL] = res;
                cb && cb(res);
            })
        }
    }

    public loadDir(path:string,cb?:(res:cc.Asset[])=>void){
        let subs = path.split(':')
        if (subs.length !== 2) {
            cc.error(`ResMgr.loadRes(${path}) is error./n eg:'name:login|hall|room'`);
            return;
        }

        if (!this._mainBundle){
            cc.error(`ResMgr.loadDir(${path}) is error.please call function ResMgr.setMainPack()`);
            return ;
        }
        let _path = `${this._packName}/res/${subs[0]}`;
        if (this._resCache[`${subs[1]}`] && this._resCache[`${subs[1]}`][_path]){
            cb && cb(this._resCache[`${subs[1]}`][_path]);
            return ;
        }

        this._mainBundle.loadDir(`res/${subs[0]}`,(e:Error,ress:cc.Asset[])=>{
            if (e){
                cc.error(`res/${subs[0]} is error.`);
                return ;
            }
            
            !this._resCache[`${subs[1]}`] && (this._resCache[`${subs[1]}`] = {});
            this._resCache[`${subs[1]}`][_path] = ress.slice(0);
            cb && cb(ress);
        });
    }

    public releaseGroup(name:string){
        let _resArray = this._resCache[`${name}`];
        if (!_resArray) return ;

        let keys = Object.keys(_resArray);
        if (keys.length > 0){
            for (let it of keys){
                this._mainBundle.release(it);
            }
            this._resCache[`${name}`] = {};
        }
    }
    /**
     * 
     * @param displayLayout 动画展示节点
     * @param path 动画目录 eg: effects/zhadan:room
     * @param armature 当前的 Armature 名称
     * @param animName 需要播放的动画名 b
     * @param playTimes 播放次数 -1 为使用配置文件中的次数 0 为无限循环播放 > 0 为动画的重复次数
     * @param call 结束回调
     */
    public displayDragonBones(displayLayout:cc.Node, path:string,armature:string, animName: string, playTimes: number = 1, call:()=>void = null){
        console.log(`AssetsMgr.displayDragonBones0(${displayLayout.name},${path},${armature},${animName},${playTimes})`);
        
        this.loadDir(path,(assets:cc.Asset[])=>{
            if (assets.length <= 0){
                call && call();
                return;
            }

            displayLayout.getComponent(dragonBones.ArmatureDisplay) && displayLayout.removeComponent(dragonBones.ArmatureDisplay);
            const ArmatureDisplay:dragonBones.ArmatureDisplay = displayLayout.addComponent(dragonBones.ArmatureDisplay); 

            for (let it of assets){
                if (it instanceof dragonBones.DragonBonesAsset) ArmatureDisplay.dragonAsset  = it;
                if (it instanceof dragonBones.DragonBonesAtlasAsset) ArmatureDisplay.dragonAtlasAsset = it;
            }

            if (ArmatureDisplay.dragonAsset && ArmatureDisplay.dragonAtlasAsset){
                console.log(`AssetsMgr.displayDragonBones1(${displayLayout.name},${path},${armature},${animName},${playTimes})`);
                
                ArmatureDisplay.armatureName = armature;// 一定要设置armatureName
                ArmatureDisplay.playAnimation(animName, playTimes);// 播放动画（同上）
                const playEndCall = (event: cc.Event)=>{
                    ArmatureDisplay.removeEventListener(dragonBones.EventObject.COMPLETE,playEndCall,this);
                    displayLayout.removeComponent(dragonBones.ArmatureDisplay);
                    call && call();
                };
                // 监听
                ArmatureDisplay.addEventListener(dragonBones.EventObject.COMPLETE, playEndCall, this);
            } else {
                console.log(`AssetsMgr.displayDragonBones2(ERROR)`);
                call && call();
            }
        });
    }

    public playDragonBones(root:cc.Node,path:string,armature:string,palyName:string,playTimes:number=1,start?:()=>void,end?:()=>void,pos:cc.Vec2=cc.v2(0,0)){
        if (!(root instanceof cc.Node)){
            console.error(`AssetsMgr.playDragonBones(root = null)`);
            return ;
        }
        
        let dragonAsset,atlasAsset;
        this.loadDir(path,(assets:cc.Asset[])=>{
            for (let it of assets){
                if (it instanceof dragonBones.DragonBonesAsset) dragonAsset  = it;
                if (it instanceof dragonBones.DragonBonesAtlasAsset) atlasAsset = it;
            }

            if (App.Util.isInvalid(dragonAsset) || App.Util.isInvalid(atlasAsset)){
                console.error(`AssetsMgr.playDragonBones(DragonBones(${path}) is invalid.)`);
                start && start();
                end && end();
                return ;
            }
            let _node = new cc.Node(path);
            _node.parent = root;
            _node.setPosition(pos || cc.v2(0,0));
            const DB = _node.addComponent<dragonBones.ArmatureDisplay>(dragonBones.ArmatureDisplay);
            DB.dragonAsset = dragonAsset;
            DB.dragonAtlasAsset = atlasAsset;
            DB.armatureName = armature;
            DB.animationName = palyName;
            DB.playTimes = playTimes;
            DB.playAnimation(palyName, playTimes);

            // 监听
            start && DB.addEventListener(dragonBones.EventObject.START, start, this);
            // 监听
            end && DB.addEventListener(dragonBones.EventObject.COMPLETE, ()=>{
                root.removeAllChildren();
                end();
            }, this);
        });
    }
}